package ch.msoftch.gui.utils;

import static org.eclipse.jdt.core.dom.Modifier.ModifierKeyword.*;
import java.beans.*;
import java.util.*;
import java.util.Map.*;
import org.eclipse.jdt.core.*;
import org.eclipse.jdt.core.dom.*;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.Modifier.*;

/**
 * Helper class for enhancing {@link ICompilationUnit}s with constants for each defined property.
 * 
 * @author hautle
 */
public class PropertyClassEnhancer extends ClassBuilder {
	/** Flag mask of static final . */
	private static final int STATIC_FINAL_FLAGS = ModifierKeyword.STATIC_KEYWORD.toFlagValue() | ModifierKeyword.FINAL_KEYWORD.toFlagValue();

	/** The constant prefix. */
	private static final String PREFIX = "ATTR_";

	/** The properties of the class. */
	private final Map<String, Property> properties = new HashMap<String, Property>();

	/**
	 * Default constructor.
	 * 
	 * @param unit The unit
	 */
	private PropertyClassEnhancer(ICompilationUnit unit) {
		setUp(unit);
	}

	/**
	 * Enhancec the given compilation unit with constants for each defined property.
	 * 
	 * @param unit The unit to enhance
	 * @throws JavaModelException
	 */
	public static void enhanceClass(ICompilationUnit unit) throws JavaModelException {
		PropertyClassEnhancer e = new PropertyClassEnhancer(unit);
		e.fetchProperties();
		e.checkExistingConstants();
		e.createMissingConstants();
		e.write();
	}

	/**
	 * Fills {@link #properties} with the names of all defined properties.
	 */
	private void fetchProperties() {
		for (MethodDeclaration m : type.getMethods())
			addPropertyFor(m);
	}

	/**
	 * Removes unused constants and flags the properties for which a constant is allready defined.<br>
	 * Uses {@link #properties} as base.
	 */
	private void checkExistingConstants() {
		for (FieldDeclaration f : type.getFields()) {
			final PropertyConstant cons = getConstant(f);
			if (cons != null)
				dropFieldWhenUnused(cons);
		}
	}

	/**
	 * Creates the missing constants.<br>
	 * Uses {@link #properties} as base.
	 */
	private void createMissingConstants() {
		for (Entry<String, Property> e : properties.entrySet()) {
			final Property prop = e.getValue();
			if (!prop.visited)
				createConstantField(e.getKey(), prop);
		}
	}

	/**
	 * Removes the given property constant if it's unused.<br>
	 * If the referenced property of the constant is found, it will be flaged.
	 * 
	 * @param f The property constant
	 */
	private void dropFieldWhenUnused(PropertyConstant f) {
		final Property value = properties.get(f.value);
		// delete the field if no property exists for it
		if (value == null) {
			f.field.delete();
			return;
		}
		// otherwise flag the method cause at least one property constant exists for it
		value.visited = true;
	}

	/**
	 * Creates a constant field for the given property.
	 * 
	 * @param name The property name
	 * @param prop The property
	 */
	private void createConstantField(String name, Property prop) {
		final FieldDeclaration field = createField("String", getConstantName(name), createString(name), PUBLIC_KEYWORD, STATIC_KEYWORD, FINAL_KEYWORD);
		final StringBuilder str = new StringBuilder("Property constant for ");
		if (addText(str, "{@link #", prop.getGetterName(), "}"))
			str.append('/');
		if (!addText(str, "{@link #", prop.getSetterName(), "}"))
			// drop previously added '/'
			str.setLength(str.length() - 1);
		str.append(". ");
		final Type ret = prop.getType();
		if (ret.isArrayType() || ret.isPrimitiveType())
			str.append("Type ").append(EclipseHelper.getFullQualifiedName(ret)).append('.');
		else
			addText(str, "Type {@link ", EclipseHelper.getFullQualifiedName(ret), "}.");
		field.setJavadoc(createJavaDoc(str.toString()));
		addFieldFirst(field);
	}

	/**
	 * Concatenates the passed strings and appends them if the middle string is not null.
	 * 
	 * @param str The string builder
	 * @param start The start part
	 * @param middle The middle part or null
	 * @param end The end part
	 * @return True if something was added
	 */
	private boolean addText(StringBuilder str, String start, String middle, String end) {
		if (middle == null)
			return false;
		str.append(start).append(middle).append(end);
		return true;
	}

	/**
	 * Returns a property constant representing the field or null if the field is not a constant.
	 * 
	 * @param f The field
	 * @return A property constant representing the field or null
	 */
	private PropertyConstant getConstant(FieldDeclaration f) {
		// check modifiers (static final)
		if ((STATIC_FINAL_FLAGS & f.getModifiers()) != STATIC_FINAL_FLAGS)
			return null;
		// check type (String)
		final String typeName = getSimpleTypeName(f.getType());
		if (!"String".equals(typeName))
			return null;
		final VariableDeclarationFragment dec = getVariableDeclaration(f);
		if (dec == null)
			return null;
		// check initialisation (just a simple String holding the property name)
		final Expression init = dec.getInitializer();
		if (!(init instanceof StringLiteral))
			return null;
		final String name = dec.getName().getFullyQualifiedName();
		// ignore constants without specified prefix
		if (!name.startsWith(PREFIX))
			return null;
		return new PropertyConstant(name, ((StringLiteral) init).getLiteralValue(), f);
	}

	/**
	 * Adds the property represented by the given method (if any).
	 * 
	 * @param m The method
	 */
	private void addPropertyFor(MethodDeclaration m) {
		final String name = m.getName().getFullyQualifiedName();
		if (name.length() < 4)
			return;
		final int argCnt = m.parameters().size();
		final boolean get = name.startsWith("get");
		final boolean is = name.startsWith("is");
		if (argCnt == 0 && (get || is) || argCnt == 1 && name.startsWith("set"))
			buildProperty(Introspector.decapitalize(name.substring(is ? 2 : 3)), m, get | is);
	}

	/**
	 * Builds and adds the property definition for the given method.
	 * 
	 * @param name The property name
	 * @param meth A corresponding setter/getter declaration
	 * @param getter True if the passed method represents the getter, false if it represents the setter
	 */
	private void buildProperty(final String name, MethodDeclaration meth, boolean getter) {
		Property pr = properties.get(name);
		if (pr == null)
			properties.put(name, pr = new Property());
		pr.setMethod(meth, getter);
	}

	/**
	 * Retruns the name for a constant for the given property.
	 * 
	 * @param property The property name
	 * @return The corresponding constant name
	 */
	private String getConstantName(String property) {
		final StringBuilder str = new StringBuilder(property.length() + 5);
		// flag indicating if the previous character was a uppercase char
		boolean upper = false;
		str.append(PREFIX);
		for (int i = 0, len = property.length(); i < len; i++) {
			final char c = property.charAt(i);
			// insert '_' only before the first uppercase char
			if (!upper & (upper = Character.isUpperCase(c)))
				str.append('_').append(c);
			else
				str.append(Character.toUpperCase(c));
		}
		return str.toString();
	}

	/**
	 * Describes a constant holding the name of a property.
	 * 
	 * @author hautle
	 */
	private static class PropertyConstant {
		/** The field name. */
		final String name;

		/** The initializer value. */
		final String value;

		/** The field itself. */
		final FieldDeclaration field;

		/**
		 * @param name The field name
		 * @param value The initializer value
		 * @param field The field itself
		 */
		public PropertyConstant(String name, String value, FieldDeclaration field) {
			this.name = name;
			this.value = value;
			this.field = field;
		}

	}

	/**
	 * Object encapsulation of a modifiable visitable flag.
	 * 
	 * @author hautle
	 */
	private static class Property {
		/** Flag inidicating if the field was visited. */
		boolean visited = false;

		/** The getter method or null. */
		private MethodDeclaration getter;

		/** The setter method or null. */
		private MethodDeclaration setter;

		/**
		 * Sets the given method as getter/setter method according to the passed flag.
		 * 
		 * @param meth A corresponding setter/getter declaration
		 * @param getter True if the passed method represents the getter, false if it represents the setter
		 */
		public void setMethod(MethodDeclaration meth, boolean getter) {
			if (getter)
				this.getter = meth;
			else
				this.setter = meth;
		}

		/**
		 * Returns the getter method name
		 * 
		 * @return The name or null
		 */
		String getGetterName() {
			if (getter == null)
				return null;
			return getter.getName().getIdentifier() + "()";
		}

		/**
		 * Returns the setter method name
		 * 
		 * @return The name or null
		 */
		String getSetterName() {
			if (setter == null)
				return null;
			StringBuilder str = new StringBuilder();
			str.append(setter.getName().getIdentifier());
			str.append("(");
			EclipseHelper.appendFullyQualifiedName(((SingleVariableDeclaration) setter.parameters().get(0)).getType(), str);
			str.append(")");
			return str.toString();
		}

		/**
		 * Returns the type of the property,
		 * 
		 * @return The property type
		 */
		Type getType() {
			if (getter != null)
				return getter.getReturnType2();
			final SingleVariableDeclaration param = (SingleVariableDeclaration) setter.parameters().get(0);
			return param.getType();
		}
	}
}
